当八哥(bug)来撩你了
八哥,传说每一位真正的程序猿都被它撩过。即便没见过属于自己的八哥,也见过别人的,若当真没被八哥撩,说明还不是真正的程序猿。
有时候你遇到的八哥撩猿技能很高超,特别是在异步程序里,若是该八哥的创造者并没有给它发放一个正确地“身份证”时,你更不容易认得它。今日,我遇到了这样一个八哥,差一点就被撩到七荤八素。
有些同学可能对此不以为然,觉得抓八哥都是信手拈来。阿驹认为,只有两种人抓八哥能如瓮中捉鳖,一是姐夫丹那样的神人,二是出身牛犊不怕虎的新人。(PS:若不知姐夫丹其人,欲知详情请公众号中回复“姐夫丹”。)
异步代码、分布式系统的调试难度
言归正传,从上方图中可以看出,输出这日志的程序内采用了异步编程的方式;而且,该程序是一个分布式系统,多个节点协同完成一个功能;另外,该条异常信息以INFO级别记录了下来。若是倒拐子长毛
的猿,已经知道了其难度。不过下面还是给新同学介绍一下难在哪里。
文化补遗:“倒拐子长毛”是一句四川话歇后语,谜底是“老手”,和网络流行语“老司机”是同义词。“倒拐子”是指手肘,都长出毛了,肯定是很老的手了。
首先,分布式系统其调试难度大,因为功能本是多个节点上的程序协同完成,在测试环境中,若非有良好的打桩和MOCK,还得恰到好处地让发生问题的那段代码被调用,否则是不可能轻易排查掉问题的。然而笨办法就是让各节点的程序都按正常运行,人肉发送能引发错误的请求,打开各节点的日志观察。
其次,如果是异步代码,系统往往会默许子例程(这里指被异步的任务)可以执行失败,因为你都提前返回让异步任务自己去运行了,它失败也未尝不可,大不了重新让它执行。所以,异步代码中要找出bug的第一个难度在于往往我们容许错误,而且掩盖了错误。第二个难度在于,被异步执行地任务失败以后不会影响到主程序的运行,即是主程序还能照常提供服务,若不是细心观察,不容易发现它已经出错了。
再次,看到上图中该条异常日志的级别,是INFO,这就是典型地没有为八哥发放正确地“身份证”,其“体型”和正常日志也没有太大的外观区别,这让你难以cat xxx.log|grep ERROR
来快速找到错误级别的日志。这时候只有上肉眼了。而且,该错误并未打印出发生异常时的调用堆栈,就算知道了是某个错误,对知道错误在哪个地方毛用没有。这一段还有个隐藏坑没说,后文谈,倒拐子长毛的猿人可能已经想到了。
最后,难度在于观察日志。上一条说到的八哥被不正确地记录,这条要说的是若是在不正确地记录情况下,你调试的程序还在不断输出日志,那你必须得在千万行日志里“明察秋毫之末”。这对这个难点,还有个小技巧,若你知道bug可能会在日志中留下某特定字符串,用以下命令来观察日志:tail -f xxxx.log|grep -C 10 'strings'
,然后重新触发bug观察日志。这样可以过滤掉很多与bug无关的日志。不明白这命令的同学可以在公众号里直接留言,此处不再费笔墨。
应当以什么心态对待八哥
假如代码欺骗了你,
不要悲伤,不要心急!
忧郁的日子里须要镇静:
相信吧,快乐的日子将会来临!
心儿永远向往着未来;
现在却常是bug。
一切都是浮云,一切都将会过去;
而那过去了的bug,都会成为亲切的怀恋。
不论是自己的八哥还是别人的八哥来撩你了,第一反应就是“一定要反抗”!八哥撩你,你不搞定它谁来?自己留下的坑悄悄咪咪修好是情理之中,若是别人留下的坑,先解决问题。
槽一定要吐,不吐不足以平民愤。这是开玩笑的说法。一定要说出来,这bug是什么、为什么、怎么样,大家共同借鉴学习,以后才能避免犯同样的错误。特别是那些由于编程逻辑思路上导致的bug,不给这类同学提醒一下,他下次遇到相同问题还是会把他自己按相同复杂的逻辑绕进去的。
最大的误区
当我们对待新手犯错误的时候,常常会发现那是非常简单而且显而易见的错误,但是新手他为何偏偏就不知道呢?不耐心的人往往会说:“你眼瞎啊?”
其实我们自己面对一个难以调试的bug手足无措时,我们就是那个新手了。往往是因为我们凭自己的经验忽略了不该忽略的日志,未关注该关注的代码,当了睁眼瞎。而八哥,正可能从你眼角刚好瞥见不到的位置撩了一下你诱人的脸颊。
放倒八哥的经验与技巧
显而易见的bug就不说了,修复它都是举手之劳,这里说说遇到不容易搞定的bug怎么办。
其一,分清楚病征和病根。当我们的程序给我们的结果不合预期时,往往就宣称程序里面有了bug,先别着急。不仅程序里有bug会导致不正常,配置文件里的配置不对也可能,操作系统环境变量不对也可能,它所依赖的其他软件库的版本不兼容也可能,甚至真的可能是机房的电路里飞进了幺蛾子。先留心观察清楚,到底现在发生的问题,就是因它而起,还是别的什么因素导致了这个问题出现。
其二,勿盲目深入代码。过早地盲目深入代码,只能是“身在此山中,云深不知处”,会被你眼前看到的,你认为看似正常的代码牵着鼻子走。这时候就成了上面说的“睁眼瞎”的一种。先跳出代码,厘清能导致该异常现象的功能模块是哪个,该功能正常情况下应该按照怎么样的输入,经过怎么样的处理,又得到何种输出? 将自己代入,假装自己是相关功能/对象/异常,然后问发出哲学终极三问。
哲学补遗:哲学终极三问:我是谁?从哪里来?到哪里去?
其三,善用工具高效地追踪与观察问题。利用好断点、单步跟踪等利器。眼过千遍,不如单步一遍。日志尽量要在关键的输入输出位置一次性正确而清晰地加上,包括在日志中暴露当前日志由哪个模块、哪个函数打印的,该日志下方接下来要处理什么,该日志上方处理了什么等等,必要时甚至需要加上线程ID号等等。各类编程语言成熟的IDE都有了可视化地下断点和单步调试功能,非常方便;用vim等文本编辑器的,也可以用gdb,pdb等调试工具,都方便,不过风格不同而已。前文提到了一个隐藏的坑,就是错误级别的日志请单独分离一份出来。
其四,当疑问有了初步把握,去求证,而非猜测。求证的过程可以通过单元测试、单步调试时临时执行证明性的代码、输入详细地日志观察等等。不要让思维盲区蒙蔽了你的双眼。有时候,问一问熟悉相关代码的老手也是很好的,他们的指点可能会让你少走弯路,不过,但是,一切以代码为准,代码说了什么就是什么。代码错了就错了,前提是你对该问题证明充足。
其五,修复bug时莫大意。当我们定位出了bug的确切位置和原因,该动手修复它的时候,多留心该段代码对别处代码的影响。例如,你要重命名的变量是否会被别处使用?导致该处错误的变量、函数、对象等,在别处有使用吗?是否有同样问题?你修改了此处代码,别处会否受到影响?一定要全项目检查清楚。另外,注意拼写错误等低级问题。
读到这里,你以为上一条已经是最后一条了吧?不,我非要再多说一条:当你用理性地头脑,清晰地梳理和证明了bug非项目代码所致,那么,请将目光向上移至用户输入端、向下移至你所依赖的底层框架,甚至再向下移至操作系统。这些软件都是人写的,世间无完美的程序,它们都可能存在bug。
再多说一点
如果在接收到本文推送就开始阅读的话,读到这里,我真的很想跟你说一句:
年!轻!人!不!要!老!熬!夜!身体不会欺骗和将就你。早歇!